package com.hippoframework.bean.processor;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

import javax.annotation.PostConstruct;

import cn.hutool.core.collection.CollectionUtil;
import com.hippoframework.exception.BeansException;
import com.hippoframework.utils.ClassUtils;
import com.hippoframework.utils.ReflectionUtils;
import lombok.Getter;
import lombok.NonNull;
import lombok.extern.slf4j.Slf4j;

/**
 * 生命周期相关的Bean后置处理器
 *
 * @author TylerW
 * @date 4/1/2020 5:39 PM
 */
@Slf4j
public class LifecycleBeanPostProcessor implements BeanPostProcessor {

    private final transient Map<Class<?>, LifecycleMetadata> lifecycleMetadataCache = new ConcurrentHashMap<>(256);

    @Override
    public void doProcessBeforeInit(Object bean, String beanName) {
        LifecycleMetadata metadata = findLifecycleMetadata(bean.getClass());
        try {
            if (metadata != null) {
                metadata.invokeInitMethods(bean, beanName);
            }
        } catch (Throwable throwable) {
            throw new BeansException(beanName, "Failed to invoke init method");
        }
    }

    private LifecycleMetadata findLifecycleMetadata(Class<?> clazz) {
        if (CollectionUtil.isEmpty(lifecycleMetadataCache)) {
            return this.buildLifecycleMetadata(clazz);
        }
        LifecycleMetadata metadata = this.lifecycleMetadataCache.get(clazz);
        if (metadata == null) {
            synchronized (this.lifecycleMetadataCache) {
                metadata = this.lifecycleMetadataCache.get(clazz);
                if (metadata == null) {
                    metadata = buildLifecycleMetadata(clazz);
                    if (metadata != null) {
                        this.lifecycleMetadataCache.put(clazz, metadata);
                    }
                }
                return metadata;
            }
        }
        return metadata;
    }

    private LifecycleMetadata buildLifecycleMetadata(final Class<?> clazz) {
        Method[] declaredMethods = clazz.getDeclaredMethods();
        List<LifecycleElement> lifecycleElements = new ArrayList<>(declaredMethods.length);
        for (Method method : declaredMethods) {
            if (method.isAnnotationPresent(PostConstruct.class)) {
                lifecycleElements.add(new LifecycleElement(method));
            }
        }
        if (CollectionUtil.isNotEmpty(lifecycleElements)) {
            lifecycleMetadataCache.put(clazz, new LifecycleMetadata(lifecycleElements));
        }
        return lifecycleMetadataCache.get(clazz);
    }

    /**
     * Bean生命周期元数据，封装生命周期相关的方法
     */
    private static class LifecycleMetadata {

        private final Collection<LifecycleElement> initMethods;

        public LifecycleMetadata(Collection<LifecycleElement> initMethods) {
            this.initMethods = initMethods;
        }

        public void invokeInitMethods(Object target, String beanName) throws Throwable {
            Collection<LifecycleElement> initMethodsToIterate = this.initMethods;
            if (!initMethodsToIterate.isEmpty()) {
                for (LifecycleElement element : initMethodsToIterate) {
                    if (log.isTraceEnabled()) {
                        log.trace("Invoking init method on bean '" + beanName + "': " + element.getMethod());
                    }
                    element.invoke(target);
                }
            }
        }

    }

    /**
     * Bean生命周期相关方法包装
     */
    private static class LifecycleElement {

        @Getter
        private final Method method;
        @Getter
        private final String identifier;

        public LifecycleElement(Method method) {
            if (method.getParameterCount() != 0) {
                throw new IllegalStateException("Lifecycle method annotation requires a no-arg method: " + method);
            }
            this.method = method;
            this.identifier = (Modifier.isPrivate(method.getModifiers()) ?
                               ClassUtils.getQualifiedMethodName(method) : method.getName());
        }

        public void invoke(Object target) throws Throwable {
            ReflectionUtils.makeAccessible(this.method);
            this.method.invoke(target, (Object[]) null);
        }

        @Override
        public boolean equals(@NonNull Object other) {
            if (this == other) {
                return true;
            }
            if (!(other instanceof LifecycleElement)) {
                return false;
            }
            LifecycleElement otherElement = (LifecycleElement) other;
            return (this.identifier.equals(otherElement.identifier));
        }

        @Override
        public int hashCode() {
            return this.identifier.hashCode();
        }
    }

}
